//==============================================================================
//
//                GPXOSM - the gpx openstreetmap class
//
//               Copyright (C) 2019  Dick van Oudheusden
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free
// Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
//
//==============================================================================

#include <string>
#include <cstring>
#include <fstream>
#include <sstream>
#include <vector>
#include <ctime>
#include <iomanip>
#include <limits>
#include <algorithm>

#include "Arguments.h"
#include "XMLParser.h"
#include "Algorithm.h"

namespace gpxtools
{


// --Track --------------------------------------------------------
class Track
{
public:
  struct Point
  {
    Point(std::string lat, std::string lon)
    {
      _lat = lat;
      _lon = lon;
    }
    Point()
    {
      clear();
    }

    void clear()
    {
      _lat.clear();
      _lon.clear();
    }

    std::string _lat;
    std::string _lon;
  };

  typedef std::vector<Point> Points;

  enum JoinResult { ERROR, UPDATED, OK };

  Track() :
    _name(),
    _type(),
    _copyright(),
    _way(),
    _relation(),
    _relations()
  {
  }

  ~Track()
  {
  }

  void setName(const std::string name) { _name = name; }

  void setType(const std::string type) { _type = type; }

  void setCopyright(const std::string copyright)
  {
    if (!copyright.empty()) _copyright = copyright;
  }

  void startRelation()
  {
    _relation._number.clear();
    _relation._points.clear();
    _relation._startPoint.clear();
    _relation._startIndex = std::string::npos;
    _relation._endPoint.clear();
    _relation._endIndex = std::string::npos;
  }

  void endRelation(const std::string &number)
  {
    _relation._number = number;

    _relations.push_back(_relation);
  }

  void startWay()
  {
    _way.clear();
  }

  void endWay()
  {
    if (join(_relation._points, _way) != ERROR)
    {
      _relation._points.insert(_relation._points.end(), _way.begin(), _way.end());
    }
  }

  void addPoint(const Point &point)
  {
    _way.push_back(point);
  }

  // Join the relations
  bool join()
  {
    bool ok = align();

    if (!ok)
    {
      report();
    }
    else
    {
      reduce();
    }

    return (ok);
  }

  void write(std::ostream &str) const
  {
    str << "<?xml version='1.0'?>" << std::endl;
    str << "<gpx version='1.1'>" << std::endl;

    if (!_copyright.empty())
    {
      str << " <metadata>" << std::endl;
      str << "  <copyright author='" << _copyright << "' />" << std::endl;
      str << " </metadata>" << std::endl;
    }

    str << " <trk>" << std::endl;

    if (!_name.empty())
    {
      str << "  <name>" << _name << "</name>" << std::endl;
    }
    if (!_type.empty())
    {
      str << "  <type>" << _type << "</type>" << std::endl;
    }

    str << "  <trkseg>" << std::endl;
    for (auto relation = _relations.begin(); relation != _relations.end(); ++relation)
    {
      for (auto point = relation->_points.begin(); point != relation->_points.end(); ++point)
      {
         str << "   <trkpt lat='" << point->_lat << "' lon='" << point->_lon << "' />" << std::endl;
      }
    }
    str << "  </trkseg>" << std::endl;
    str << " </trk>" << std::endl;
    str << "</gpx>" << std::endl;
  }

private:
  struct Relation
  {
    std::string  _number;
    Points       _points;
    Point        _startPoint;
    size_t       _startIndex;
    Point        _endPoint;
    size_t       _endIndex;
  };

  // Align the relations
  bool align()
  {
    bool updated = false;
    bool error   = false;

    // Join the relations
    do
    {
      updated = false;
      error   = false;

      auto previous = _relations.begin();
      auto next     = (previous != _relations.end() ? previous + 1 : _relations.end());

      for (; next != _relations.end(); ++previous, ++next)
      {
        switch(join(*previous, *next))
        {
          case ERROR  : error   = true; break;
          case UPDATED: updated = true; break;
          case OK     :                 break;
        }
      }
    }
    while (updated);

    return (!error);
  }

  // Report unjoined relations
  void report()
  {
    auto previous = _relations.begin();
    auto next     = (previous != _relations.end() ? previous + 1 : _relations.end());

    for (; next != _relations.end(); ++previous, ++next)
    {
      if (join(previous->_points, next->_points) == ERROR)
      {
        std::cerr << "Unable to join relations " << previous->_number << " and " << next->_number << ", use join point" << std::endl;
      }
    }
  }

  // Reduce the tracks with indices
  void reduce()
  {
    for (auto relation = _relations.begin(); relation != _relations.end(); ++relation)
    {
      size_t startIndex = relation->_startIndex;
      size_t endIndex   = relation->_endIndex;

      if ((startIndex != std::string::npos) || (endIndex != std::string::npos))
      {
        if ((startIndex != std::string::npos) &&
            (endIndex   != std::string::npos) &&
            (startIndex  > endIndex))
        {
          std::reverse(relation->_points.begin(), relation->_points.end());

          startIndex = relation->_points.size() - startIndex;
          endIndex   = relation->_points.size() - endIndex;
        }

        auto start = (startIndex != std::string::npos) ?
              relation->_points.begin() + long(startIndex) : relation->_points.begin();
        auto end   = (endIndex != std::string::npos) ?
              relation->_points.begin() + long(endIndex) : relation->_points.end();

        relation->_points = Points(start, end);
      }
    }
  }

  static double calcDistance(const Point &first, const Point &second)
  {
    try
    {
      return gpx::calcDistance(std::stod(first._lat), std::stod(first._lon), std::stod(second._lat), std::stod(second._lon));
    }
    catch (...)
    {
      return std::numeric_limits<double>::max();
    }
  }

  static double calcDistance(const Point &point, const Points &points, size_t &index)
  {
    double minDistance = std::numeric_limits<double>::max();

    for (auto iter = points.begin(); iter != points.end(); ++iter)
    {
      double distance = calcDistance(*iter, point);

      if (distance < minDistance)
      {
        minDistance = distance;
        index       = iter - points.begin();
      }
    }

    return minDistance;
  }

  // Join the ways
  JoinResult join(Points &previous, Points &next)
  {
    JoinResult result = OK;

    if (previous.empty())
    {
      // Okee, first track
    }
    else if (calcDistance(previous.back(), next.front()) < 10.0)
    {
      // Okee, inline
    }
    else if (calcDistance(previous.back(), next.back()) < 10.0)
    {
      std::reverse(next.begin(), next.end());
      result = UPDATED;
    }
    else if (calcDistance(previous.front(), next.front()) < 10.0)
    {
      std::reverse(previous.begin(), previous.end());
      result = UPDATED;
    }
    else if (calcDistance(previous.front(), next.back()) < 10.0)
    {
      std::reverse(previous.begin(), previous.end());
      std::reverse(next.begin(), next.end());
      result = UPDATED;
    }
    else
    {
      result = ERROR;
    }

    return result;
  }

  // Join relations
  JoinResult join(Relation &previous, Relation &next)
  {
    JoinResult  result = OK;
    std::size_t index  = std::string::npos;

    if (calcDistance(previous._points.back(), next._points.front()) < 10.0)
    {
      // Okee, inline
    }
    else if (calcDistance(previous._points.back(), next._points.back()) < 10.0)
    {
      std::reverse(next._points.begin(), next._points.end());
      result = UPDATED;
    }
    else if (calcDistance(previous._points.front(), next._points.front()) < 10.0)
    {
      std::reverse(previous._points.begin(), previous._points.end());
      result = UPDATED;
    }
    else if (calcDistance(previous._points.front(), next._points.back()) < 10.0)
    {
      std::reverse(previous._points.begin(), previous._points.end());
      std::reverse(next._points.begin(), next._points.end());
      result = UPDATED;
    }
    else if (calcDistance(previous._points.back(), next._points, index) < 10.0)
    {
      next._startPoint = previous._points.back();
      next._startIndex = index;
    }
    else if (calcDistance(previous._points.front(), next._points, index) < 10.0)
    {
      std::reverse(previous._points.begin(), previous._points.end());
       next._startPoint = previous._points.back();
       next._startIndex = index;
      result = UPDATED;
    }
    else if (calcDistance(next._points.front(), previous._points, index) < 10.0)
    {
      previous._endPoint = next._points.front();
      previous._endIndex = index;
    }
    else if (calcDistance(next._points.back(), previous._points, index) < 10.0)
    {
      std::reverse(next._points.begin(), next._points.end());
      previous._endPoint = next._points.front();
      previous._endIndex = index;
      result = UPDATED;
    }
    else
    {
      result = ERROR;
    }

    return result;
  }

private:
  std::string _name;
  std::string _type;
  std::string _copyright;

  Points      _way;


  Relation              _relation;
  std::vector<Relation> _relations;
};



// -- Way ---------------------------------------------------------
class Way : public XMLParserHandler
{
public:
  Way(const std::string &number, Track &track) :
    _track(track),
    _number(number),
    _xmlParser(this),
    _ref(nullptr)
  {
  }

  bool download()
  {
    bool ok = false;

    std::string cmd = "wget -q -O - http://www.openstreetmap.org/api/0.6/way/" + _number + "/full";

    FILE *stream = popen(cmd.c_str(), "r");
    if (stream != nullptr)
    {
      char buffer[4096];

      ok = true;
      while (ok && (fgets(buffer, sizeof(buffer), stream) != nullptr))
      {
        //std::cout << buffer << std::endl;
        ok = _xmlParser.parse(buffer, false);
      }

      if (ok)
        _xmlParser.parse("", true);

      pclose(stream);
    }
    else
    {
      std::cerr << "Unable to start: " << cmd << std::endl;
    }

    return ok;
  }

private:
  std::string fetch(const Attributes &attributes, const std::string &key)
  {
    auto attribute = attributes.find(key);

    return (attribute != attributes.end() ? attribute->second : "");
  }

  // Interface XMLParserHandler
  virtual void startElement(const std::string &path, const std::string &, const Attributes &attributes)
  {
    if (path == "/osm")
    {
      _ref = new Ref();
    }

    if (path == "/osm/node")
    {
      std::string lat = fetch(attributes, "lat");
      std::string lon = fetch(attributes, "lon");
      std::string id  = fetch(attributes, "id");

      if (!lat.empty() && !lon.empty() && !id.empty())
      {
        //std::cout << "Point:" << lat << ',' << lon << std::endl;
        (*_ref)[id] = Track::Point(lat, lon);
      }
    }

    if (path == "/osm/way/nd")
    {
      std::string ref  = fetch(attributes, "ref");

      if (!ref.empty() && _ref->find(ref) != _ref->end())
      {
        auto point = _ref->find(ref);

        if (point != _ref->end())
        {
          _track.addPoint(point->second);
        }
      }
    }
  }

  virtual void endElement(const std::string &path, const std::string &)
  {
    if (path == "/osm")
    {
      delete _ref; _ref = nullptr;
    }
  }

  virtual void text(const std::string &, const std::string &) { }

private:
  Track           &_track;
  std::string      _number;

  XMLParser        _xmlParser;

  typedef std::map<std::string, Track::Point>         Ref;

  Ref    *_ref;

  Way();
};



// -- Relation ----------------------------------------------------
class Relation : public XMLParserHandler
{
public:
  Relation(Track &track) :
    _track(track),
    _xmlParser(this)
  {
  }

  bool download(const std::string &number)
  {
    bool ok = false;

    std::string cmd = "wget -q -O - http://www.openstreetmap.org/api/0.6/relation/" + number;

    FILE *stream = popen(cmd.c_str(), "r");
    if (stream != nullptr)
    {
      char buffer[4096];

      ok = true;
      while (ok && (fgets(buffer, sizeof(buffer), stream) != nullptr))
      {
        ok = _xmlParser.parse(buffer, false);
      }

      if (ok)
        _xmlParser.parse("", true);

      pclose(stream);
    }
    else
    {
      std::cerr << "Unable to start: " << cmd << std::endl;
    }

    _track.startRelation();
    for (auto way = _ways.begin(); way != _ways.end(); ++way)
    {
      _track.startWay();

      (*way)->download();

      _track.endWay();
    }
    _track.endRelation(number);

    return ok;
  }

private:
  std::string fetch(const Attributes &attributes, const std::string &key)
  {
    auto attribute = attributes.find(key);

    return (attribute != attributes.end() ? attribute->second : "");
  }

  // Interface XMLParserHandler
  virtual void startElement(const std::string &path, const std::string &, const Attributes &attributes)
  {
    if (path == "/osm")
    {
      _track.setCopyright(fetch(attributes, "copyright"));
    }

    if (path == "/osm/relation/member")
    {
      if (fetch(attributes, "type") == "way")
      {
        std::string way = fetch(attributes, "ref");

        //std::cout << "Way:" << way << std::endl;
        try
        {
          std::stol(way);

          _ways.push_back(new Way(way, _track));
        }
        catch(...)
        {
          std::cerr << "Invalid way: " << way << " in relation ignored." << std::endl;
        }
      }
    }
  }

  virtual void endElement(const std::string &, const std::string &) { }

  virtual void text(const std::string &, const std::string &) { }

private:
  Track            &_track;

  XMLParser         _xmlParser;

  std::vector<Way*> _ways;

  Relation();
};



// -- GPXOSM ------------------------------------------------------
class GPXOSM
{
public:
  GPXOSM() :
    _arguments("gpxosm [OPTION].. [RELATION]..\nDownload and combine OSM relations to a GPX-file.\n", "gpxosm v0.1",
               "Generate a GPX-file on standard output based on a sequence on Openstreetmap relations."),
    _name   (_arguments, true,  'n', "name",    "NAME",           "set the name of the GPX-track", ""),
    _type   (_arguments, true,  't', "type",    "TYPE",           "set the type of the GPX-track", "")
  {
  }

  ~GPXOSM()
  {
  }

  bool processArguments(int argc, char *argv[])
  {
    std::vector<std::string> relations;

    if (!_arguments.parse(argc,argv, relations))
    {
      return false;
    }
    else
    {
      gpxtools::Track track;

      track.setName(_name.value());
      track.setType(_type.value());

      download(relations, track);

      if (track.join())
      {
        track.write(std::cout);
      }
    }

    return true;
  }

private:
  void download(const std::vector<std::string> &relations, Track &track)
  {
    for (auto number = relations.begin(); number != relations.end(); ++number)
    {
      try
      {
        std::stol(*number);

        Relation relation(track);

        relation.download(*number);
      }
      catch(...)
      {
        std::cerr << "Invalid relation: " << *number << " ignored." << std::endl;
      }
    }
  }

private:
  arg::Arguments           _arguments;

  arg::Argument            _name;
  arg::Argument            _type;
};
}



int main(int argc, char *argv[])
{
  gpxtools::GPXOSM gpxOSM;

  if (gpxOSM.processArguments(argc, argv))
  {
    return 0;
  }

  return 1;
}

#if 0
<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6" generator="CGImap 0.6.1 (1898 thorn-03.openstreetmap.org)" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
 <relation id="9327" visible="true" version="846" changeset="65105226" timestamp="2018-12-02T21:02:37Z" user="SomeoneElse" uid="61942">
  <member type="way" ref="26730670" role=""/>
  <member type="way" ref="26730671" role=""/>
  <member type="way" ref="91649821" role=""/>
  <member type="way" ref="529697769" role=""/>
  <tag k="name" v="National Byway (Yorkshire)"/>
  <tag k="network" v="rcn"/>
  <tag k="operator" v="National Byway"/>
  <tag k="ref" v="NB"/>
  <tag k="route" v="bicycle"/>
  <tag k="type" v="route"/>
 </relation>
</osm>

<?xml version="1.0" encoding="UTF-8"?>
<osm version="0.6" generator="CGImap 0.6.1 (1909 thorn-01.openstreetmap.org)" copyright="OpenStreetMap and contributors" attribution="http://www.openstreetmap.org/copyright" license="http://opendatacommons.org/licenses/odbl/1-0/">
 <node id="42881242" visible="true" version="6" changeset="34691835" timestamp="2015-10-17T11:28:08Z" user="vindert" uid="1683540" lat="51.5174500" lon="4.2908800">
  <tag k="expected_rwn_route_relations" v="3"/>
  <tag k="rwn_ref" v="94"/>
 </node>
 <node id="42881332" visible="true" version="3" changeset="3451199" timestamp="2009-12-25T22:27:26Z" user="AND_fixbot" uid="211771" lat="51.5174900" lon="4.2909200"/>
 <way id="231747375" visible="true" version="4" changeset="27197750" timestamp="2014-12-03T11:29:43Z" user="It's so funny_mechanical" uid="2394881">
  <nd ref="42881332"/>
  <tag k="highway" v="tertiary"/>
  <tag k="name" v="Ligneweg"/>
 </way>
</osm>
#endif
